본문으로 건너뛰기

23.05.29

오늘 한 일

  • 알고리즘 문제 풀이
  • 학교 시험 공부(알고리즘, 소프트웨어공학)
  • 익스텐션 제출 여부/시청 여부가 제대로 표시되지 않은 오류 수정
  • 카공실록 리뷰 작성 페이지 구현 마무리
  • 리액트 SPA 구현
  • nest.js 강의
  • 블로그 글 쓰기

리액트 SPA 구현

바닐라 JS로 리액트 만드는 스터디 - 6주차

PR 링크

SPA를 구현하기 위해 리액트에서 사용하는 react-router-dom의 코드를 까보았다.

// react-router-dom/index.tsx
export function BrowserRouter({ basename, children, window }: BrowserRouterProps) {
let historyRef = React.useRef<BrowserHistory>();
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ window, v5Compat: true });
}

let history = historyRef.current;
let [state, setState] = React.useState({
action: history.action,
location: history.location,
});

React.useLayoutEffect(() => history.listen(setState), [history]);

return (
<Router
basename={basename}
children={children}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
}
// router/history.ts
export function createBrowserHistory(options: BrowserHistoryOptions = {}): BrowserHistory {
function createBrowserLocation(window: Window, globalHistory: Window['history']) {
let { pathname, search, hash } = window.location;
return createLocation(
'',
{ pathname, search, hash },
// state defaults to `null` because `window.history.state` does
(globalHistory.state && globalHistory.state.usr) || null,
(globalHistory.state && globalHistory.state.key) || 'default'
);
}

function createBrowserHref(window: Window, to: To) {
return typeof to === 'string' ? to : createPath(to);
}

return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
}

함수를 따라 더 들어가보니 window.history 객체를 사용하는 것을 확인할 수 있었다.

window.history 객체는 브라우저의 세션 기록을 저장하는 객체이다.
pushState 메서드를 통해 세션 기록을 추가할 수 있고,
popstate 이벤트를 통해 세션 기록을 불러올 수 있다.

그럼 이 API를 가지고 구현하면 되겠다.

형태는 react-router-domCreateBrowserRouter와 비슷하게 만들어보았다.

다음은 react-router-domCreateBrowserRouter의 예시이다.

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

import Root, { rootLoader } from './routes/root';
import Team, { teamLoader } from './routes/team';

const router = createBrowserRouter([
{
path: '/',
element: <Root />,
loader: rootLoader,
children: [
{
path: 'team',
element: <Team />,
loader: teamLoader,
},
],
},
]);

ReactDOM.createRoot(document.getElementById('root')).render(<RouterProvider router={router} />);

나는 인자로 pathelement를 받는 객체를 배열로 넘겨주는 방식으로 구현했다.

import { render } from '../Kreact';

export default function createRouter(root, routes = []) {
const history = window.history;
const routeMap = new Map();

routes.forEach(({ pathname, element }) => {
routeMap.set(pathname, element);
});

function push(pathname, state) {
history.pushState(state, null, pathname);

_render(root, pathname);
}

window.addEventListener('popstate', () => {
_render(root, window.location.pathname);
});

function _render(root, pathname) {
const element = routeMap.get(pathname);
if (!element) throw new Error('NOT FOUND');

root.innerHTML = '';
render(root, element);
}

return {
push,
};
}

그럼 이제 index.js에서 createRouter를 사용해보자.

// src/index.js
import Kreact, { render } from './Kreact';
import createRouter from './Kreact-router';
import About from './pages/About';
import Contact from './pages/Contact';
import Home from './pages/Home';

export const router = createRouter(document.getElementById('root'), [
{
pathname: '/',
element: Home,
},
{
pathname: '/about',
element: About,
},
{
pathname: '/contact',
element: Contact,
},
]);

const root = document.getElementById('root');
render(root, Home);

createBrowserRouter에서는 Provider로 router를 넘겨주었지만, 나는 일단 router 객체를 export해서 사용해보았다.

// src/components/NavBar.js
import Kreact from '../Kreact';
import { router } from '..';

export default function NavBar() {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
border: '1px solid black',
padding: '10px',
margin: '10px',
}}
>
<li onClick={() => router.push('/')}>home</li>
<li onClick={() => router.push('/about')}>about</li>
<li onClick={() => router.push('/contact')}>contact</li>
</div>
);
}

이렇게 해서 각 페이지마다 NavBar를 넣어주어서 라우팅을 구현해보았다.

익스텐션 오류 해결

사용자분들이 피드백을 남겨주셨다.

MOOC 강의 시청 여부가 다르게 표시되는 문제

구글 폼에 피드백을 받고있는데, 이런 문제를 겪고 있는 사용자가 여러 명 있었다.

스크린샷까지 남겨주었지만, 내 로직에는 문제가 없어보였다.

다행히, 소프트웨어과 친구가 해당 강의를 수강하고 있어서 잠시 빌려 테스트를 진행할 수 있었다!!

그랬더니, 내가 생각했던 것과는 다른 문제가 발생했다.

기존의 방식

과제 데이터 가져오는 법

  1. 강의 메인 페이지에서 과제/녹화강의의 id, title, 시작일, 종료일을 가져온다.
  2. 과제 - 과제 페이지에서 title과 제출 여부를 가져온다.
  3. 녹화강의 - 학습진도현황 or 온라인출석부 페이지에서 title과 시청 여부를 가져온다.
  4. 2, 3번에서 가져온 데이터를 합쳐서 반환한다. 이때 title을 기준으로 합친다.

문제점

과제/녹화강의의 제목(title)을 가지고 매칭시켜주었는데, 이게 문제였다.

과제/녹화강의의 제목은 고유한 값이 아니기 때문에 같은 제목을 가진 강의가 있으면 문제가 발생한다.

id를 가지고 매칭시켜주면 문제가 해결될 것 같았다.

하지만, 학습진도현황 or 온라인출석부 페이지에서 녹화강의 id를 가져올 수 없기 때문에 난감했다.

해결 방법

내가 할 수 있는 최선의 방법은 과제/녹화강의가 추가된 주차도 데이터에 추가하여 제목(title)과 주차제목(sectionTitle)을 가지고 매칭시켜주는 것이었다.

이렇게 하면, 같은 제목을 가진 과제가 있어도 주차가 다르기 때문에 문제가 발생하지 않는다.

같은 제목을 가진 과제인데 같은 주차다? 이러면 답이 없긴 하다...ㅋㅋㅋ

크롤링 로직 수정해서 v1.0.8.1 배포했다.

아직 해결안한 문제

팀플 과제는 여러 명 중 한 명만 제출하면 된다. 해당 과제 페이지에 들어가면 제출한 것으로 뜬다.

근데, 과제 페이지에서 확인했을 때는 미제출 과제로 표시된다.

이건 내문제가 아니라 사이버캠퍼스 시스템 문제다. 해결하는 방법은 두 가지다.

  1. 과제를 하나하나 들어가서 제출 여부를 확인한다.
  • 이건 과제가 많을 수록 많은 요청을 하기 때문에 비효율적이다. PASS
  1. 사용자가 제출 여부를 직접 수정한다.
  • 특정 과제를 선택해서 제출 여부를 수정할 수 있도록 구현한다.
  • 이럴려면 UI 수정이 조금 필요한데, ... svg를 누르면 수정할 수 있도록 구현하면 될 것 같다.
  • 수정한 데이터는 chrome.storage에 저장해서 사용하면 될 것 같다.

내일 할 일

  • 알고리즘 문제 풀이
  • Nest.js 강의 무조건 듣기
  • 함수형 프로그래밍 스터디 진행
  • 블로그 글 쓰기